/* 
 * File:   Interpret.cpp
 * Author: weeteck
 * 
 * Created on 18. listopad 2012, 0:52
 */

#include <stdio.h>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <list>
#include <string.h>
#include <string>
#include <errno.h>
#include <fcntl.h>

#include "JobControl.h"
#include "Job.h"
#include "SingleCommand.h"
#include "Inter.h"
#include "Interpret.h"

#define I_AM_CHILD 0
#define P_IN 1
#define P_OUT 0

using std::string;
using std::cout;
using std::cerr;
using std::endl;


/**
 * Funkce spouštějící interpretaci sekvence příkazů (kolony).
 * @param cmdLine Sekvence příkazů
 */
void
Interpret::interpretCmd(cmdList & cmdLine)
{
	bool piped = cmdLine.size() > 1;

	if (cmdLine.size() < 1) // size otestovano v xxShell
	{
		/* Prazdny vstup -> pryc */
		return;
	} else if (!piped)
	{
		/* Pokud by to byl 1 interni prikaz -> provest */
		SingleCommand* pSc = cmdLine.front();
		const char* cmdName = pSc->getArgs().front().c_str();

		if (is_builtin_command(cmdName))
		{
			/* zaloha stdin, stdout a stderr
			 * provedeni prikazu by mohlo nektere dulezite deskriptory uzavrit */
			int stdinSave = dup(STDIN_FILENO);
			int stdoutSave = dup(STDOUT_FILENO);
			int stderrSeve = dup(STDERR_FILENO);

			interpretCmd(cmdLine, cmdLine.rbegin(), false);

			redirectDescr(stdinSave, STDIN_FILENO);
			redirectDescr(stdoutSave, STDOUT_FILENO);
			redirectDescr(stderrSeve, STDERR_FILENO);

			return;
		}
	} else
	{
		/* Kontrola proti internim prikazum v kolone */
		for (cmdList::const_iterator it = cmdLine.begin(); it != cmdLine.end(); ++it)
		{
			SingleCommand* pSc = *it;
			const char* cmdName = pSc->getArgs().front().c_str();

			if (is_builtin_command(cmdName))
			{
				cerr << "xxShell: Kolona nesmi obsahovat vestavene prikazy." << endl;
				return;
			}
		}
	}

	pid_t pid = fork();
	if (pid == I_AM_CHILD)
	{
		set_sighdls(false);
		if (setpgid(0, 0) < 0)
			perror("setpgid child");
		interpretCmd(cmdLine, cmdLine.rbegin(), true);
	}

	/* I AM PARENT */
	if (setpgid(pid, pid) < 0)
		perror("setpgid parrent");
	if (cmdLine.back()->isInBackground())
	{
    string cmd = cmdStr(cmdLine);
		//const char *cmd = cmdStr(cmdLine).c_str();
		printf("[%d]  %s: %d\n", last_job_id, piped ? "pgid" : "pid", (int) pid);
		jobs_list.push_back(new Job(++last_job_id, (int) pid, cmd.c_str(), Job::RUNNING, piped));
	}
  else
	{
		int status = wait_for_child(pid);
		if (test_child_status(status, NULL) == Job::STOPPED)
		{
      string cmd = cmdStr(cmdLine);
			//const char *cmd = cmdStr(cmdLine).c_str();
			printf("%s  (%s: %d)\n", cmd.c_str(), piped ? "pgid" : "pid", (int) pid);
			jobs_list.push_back(new Job(++last_job_id, (int) pid, cmd.c_str(), Job::STOPPED, piped));
		}
	}

}


/**
 * Rekurzivní funkce, která přímo provádí interní příkazy, nebo spouští 
 * provedení externích příkazů. Provádí se vždy ten, na který ukazuje 
 * iterátor. První se provádí poslední v sekvenci, rekurzivním voláním funkce 
 * s posunutým iterátorem se pak spouští ten v sekvenci předcházející v NOVÉM
 * PROCESU, až se dojde k prvnímu. 
 * 
 * Procesu nastavuje STDIN, STDOUT, (příp. STDERR) pro přenos mezi procesy a 
 * přsměrování ze/do souboru. 
 * Možnosti vstupu: STDIN, ze souboru, od předcházejícího příkazu (pipe).
 * Možnosti výstupu: STDOUT/STDERR, do souboru, k následujícímu příkazu (pipe).
 * 
 * @param cmdLine Sekvence příkazů
 * @param iter Iterátor nastavený na aktuální příkaz
 * @param child False = není potomkem shellu ani příkazu, True = je něčím 
 * potomkem
 */
void
Interpret::interpretCmd(cmdList & cmdLine, cmdIterator iter, bool child)
{
	SingleCommand & aktualCmd = **iter;
	int nargs;
	char** args = aktualCmd.getArgsToArray(nargs);

	bool isFirst = (*cmdLine.begin() == *iter);
	bool isLast = (cmdLine.rbegin() == iter);

	int exitStatus = EXIT_SUCCESS;

	try
	{
		if (isFirst)
		{
			// Presmerovani STDIN
			fileToStdIn(aktualCmd.getFileIn());
		} else // !isFirst
		{
			// spusteni potomka a propojeni s vyuzitim pipe
			piping(cmdLine, iter);
		}

		if (isLast)
		{
			// Presmerovani STDOUT
			stdOutToFile(aktualCmd);
		}
		
		// Presmerovani STDERR
		stdErrToFile(aktualCmd);

		// PROVEDENI PRIKAZU
		if (!builtin_command(args, nargs))
		{
			// spusteni externiho prikazu
			execvp(args[0], args);
			// jestli jsme tady, nastala chyba
			throw InterpretException("xxShell: Prikaz '" + string(args[0]) + "' nebyl nalezen.");
		}
	} catch (InterpretException& ex)
	{
		string errMsg = ex.getText();
		if (errMsg != "")
		{
			cerr << errMsg << endl;
		}

		// poslat EOF aby ty dalsi v kolone necekaly
		if (!isLast)
		{
			char eof[2];
			eof[0] = EOF;
			eof[1] = '\0';
			cout << eof << endl;
		}

		exitStatus = EXIT_FAILURE;
	}

	// zavreni deskriptoru
	close(STDIN_FILENO);
	close(STDOUT_FILENO);
	close(STDERR_FILENO);

	// uvolneni pameti
	SingleCommand::destroyArgsArray(args, nargs);
	args = NULL;

	if (child)
	{
		destroyCmdLine(cmdLine);
		exit(exitStatus);
	} else
		return;
}


/**
 * Rutina která eventuelně přesměruje soubor na standardní vstup procesu - 
 * vykonávaného příkazu. Když fileName je "", neděje se nic.
 * 
 * @param fileName název souboru k přesměrování
 */
void
Interpret::fileToStdIn(string fileName)
{
	if (fileName != "")
	{
		int fileInD = open(fileName.c_str(), O_RDONLY);
		if (fileInD < 0)
		{
			perror(fileName.c_str());
			throw InterpretException();
		}
		redirectDescr(fileInD, STDIN_FILENO);
	}
}


/**
 * Rutina vytvoří rouru a nový proces, který bude vykonávat proces v 
 * sekvenci předcházející. Zajistí přesměrování deskriptorů tak, aby 
 * STDOUT nového procesu přišel (přes rouru) na STDIN aktuálního procesu. 
 * 
 * @param cmdLine celá sekvence příkazů
 * @param iter aktuální pozice iterátoru
 */
void
Interpret::piping(cmdList & cmdLine, cmdIterator iter)
{
	int pipeD[2];
	pipe(pipeD);

	if (fork() == I_AM_CHILD)
	{
		close(pipeD[P_OUT]);
		redirectDescr(pipeD[P_IN], STDOUT_FILENO);
		interpretCmd(cmdLine, ++iter, true);
	}

	// I AM PARENT
	close(pipeD[P_IN]);
	redirectDescr(pipeD[P_OUT], STDIN_FILENO);
}


/**
 * Rutina která eventuelně přesměruje STDOUT procesu do 
 * souboru. Jména souborů získává z objektu aktualCmd. 
 * Když některé jméno je "", neděje se nic.
 * 
 * @param aktualCmd odkaz na aktuální příkaz
 */
void
Interpret::stdOutToFile(SingleCommand & aktualCmd)
{
	// Presmerovani STDOUT
	string fileName = aktualCmd.getFileOut();
	int writeMethod, flags, mode;

	if (fileName != "")
	{
		writeMethod = (aktualCmd.isFileOutAppend()) ? O_APPEND : O_TRUNC;
		flags = O_WRONLY | O_CREAT | writeMethod;
		mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;

		int fileOutD = open(fileName.c_str(), flags, mode);
		if (fileOutD < 0)
		{
			perror(fileName.c_str());
			throw InterpretException();
		}
		redirectDescr(fileOutD, STDOUT_FILENO);
	}
}

/**
   * Rutina která eventuelně přesměruje STDERR procesu do 
   * souboru. Jména souborů získává z objektu aktualCmd. 
   * Když některé jméno je "", neděje se nic.
   * 
   * @param aktualCmd odkaz na aktuální příkaz
   */
void
Interpret::stdErrToFile(SingleCommand & aktualCmd)
{
	string fileName = aktualCmd.getFileOut();
	int writeMethod, flags, mode;

	// Presmerovani STDERR
	fileName = aktualCmd.getFileErr();

	if (fileName != "")
	{
		writeMethod = (aktualCmd.isFileErrAppend()) ? O_APPEND : O_TRUNC;
		flags = O_WRONLY | O_CREAT | writeMethod;
		mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;

		int fileErrD = open(fileName.c_str(), flags, mode);
		if (fileErrD < 0)
		{
			perror(fileName.c_str());
			throw InterpretException();
		}
		redirectDescr(fileErrD, STDERR_FILENO);
	}
}


/**
 * Rutina která uvolní paměť po zadaném seznamu (sekvence příkazů). To by 
 * se mělo dít ve všech procesech kromě hlavního (proces Shellu).
 * 
 * @param cmdLine odkaz na seznam příkazů
 */
void
Interpret::destroyCmdLine(cmdList & cmdLine)
{
	for (cmdIterator it = cmdLine.rbegin(); it != cmdLine.rend(); ++it)
	{
		if ((*it) != NULL)
		{
			delete (*it);
			(*it) = NULL;
		}
	}
	delete(&cmdLine);
}


/**
 * Funkce zjisti jestli, zadany prikaz je vestaveny prikaz shellu.
 * 
 * @param args  Nazev prikazu z prikazove radky.
 * @return true  pokud patri mezi vestavene prikazy
 *         false pokud nepatri
 */
bool
Interpret::is_builtin_command(const char* const command)
{
	if (!strcmp(command, "exit"))
	{
		return true;
	}
	if (!strcmp(command, "cd"))
	{
		return true;
	}
	if (!strcmp(command, "bg"))
	{
		return true;
	}
	if (!strcmp(command, "fg"))
	{
		return true;
	}
	if (!strcmp(command, "jobs"))
	{
		return true;
	}
	if (!strcmp(command, "history"))
	{
		return true;
	}
	if (!strcmp(command, "help"))
	{
		return true;
	}

	return false;
}


/**
 * Funkce provede vestavene prikazy shellu.
 * 
 * @param args  Argumenty z prikazove radky.
 * @param nargs Pocet argumentu.
 * @return 1  pokud byl proveden nektery z vestavenych prikazu
 *         0  jinak
 */
int
Interpret::builtin_command(char* args[], int nargs)
{
	char* command = args[0];

	if (!strcmp(command, "exit"))
	{
		Inter::exit_shell();
		return 1;
	}
	if (!strcmp(command, "cd"))
	{
		Inter::cd(args, nargs);
		return 1;
	}
	if (!strcmp(command, "bg"))
	{
		Inter::bg(args, nargs);
		return 1;
	}
	if (!strcmp(command, "fg"))
	{
		Inter::fg(args, nargs);
		return 1;
	}
	if (!strcmp(command, "jobs"))
	{
		Inter::jobs();
		return 1;
	}
	if (!strcmp(command, "history"))
	{
		Inter::history();
		return 1;
	}
	if (!strcmp(command, "help"))
	{
		Inter::help(args, nargs);
		return 1;
	}

	return 0;
}


/**
 * V aktuálním vlákně uzavře deskriptor a na jeho místo přesměruje nový. 
 * 
 * @param sourceDescr deskriptor, který bude přesměrován
 * @param destDescr deskriptor, který bude uzavřen
 */
void
Interpret::redirectDescr(int sourceDescr, int destDescr)
{
	if (sourceDescr == destDescr)
		return;

	if (dup2(sourceDescr, destDescr) < 0)
	{
		fprintf(stderr, "Nepodarilo se presmerovani descriptoru %d > %d\n",
						sourceDescr, destDescr);
	}
	if (close(sourceDescr) < 0)
	{
		fprintf(stderr, "Nepodarilo se uzavrit descriptor %d\n", sourceDescr);
	}
}


/**
 * Funkce zrekonstruuje retezec s prikazem, ktery byl pri analyze rozparsovan na list.
 * 
 * @param list Rozparsovany prikaz.
 * @return Retzec obsahujici puvodni prikaz.
 */
string
Interpret::cmdStr(cmdList& list)
{
	string cmd;
	for (std::list<SingleCommand*>::const_iterator it = list.begin(); it != list.end(); it++)
		cmd += (*it)->getArgsToString() + " | ";
	cmd.erase(cmd.size() - 3);
	return cmd;
}

